package org.codehaus.mojo.gwt; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.Charset; import java.text.MessageFormat; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.codehaus.plexus.util.Scanner; import org.sonatype.plexus.build.incremental.BuildContext; import com.thoughtworks.qdox.JavaDocBuilder; import com.thoughtworks.qdox.model.Annotation; import com.thoughtworks.qdox.model.JavaClass; import com.thoughtworks.qdox.model.JavaMethod; import com.thoughtworks.qdox.model.JavaParameter; import com.thoughtworks.qdox.model.Type; /** * Goal which generate Async interface. * * @author <a href="mailto:nicolas@apache.org">Nicolas De Loof</a> * @version $Id$ */ @Mojo(name = "generateAsync", defaultPhase = LifecyclePhase.GENERATE_SOURCES, requiresDependencyResolution = ResolutionScope.COMPILE, threadSafe = true) public class GenerateAsyncMojo extends AbstractGwtMojo { private static final String REMOTE_SERVICE_INTERFACE = "com.google.gwt.user.client.rpc.RemoteService"; private final static Map<String, String> WRAPPERS = new HashMap<String, String>(); static { WRAPPERS.put( "boolean", Boolean.class.getName() ); WRAPPERS.put( "byte", Byte.class.getName() ); WRAPPERS.put( "char", Character.class.getName() ); WRAPPERS.put( "short", Short.class.getName() ); WRAPPERS.put( "int", Integer.class.getName() ); WRAPPERS.put( "long", Long.class.getName() ); WRAPPERS.put( "float", Float.class.getName() ); WRAPPERS.put( "double", Double.class.getName() ); } /** * Pattern for GWT service interface */ @Parameter(defaultValue = "**/*Service.java") private String servicePattern; /** * Return a com.google.gwt.http.client.Request on async interface to allow cancellation. */ @Parameter(defaultValue = "false") private boolean returnRequest; /** * A (MessageFormat) Pattern to get the GWT-RPC servlet URL based on service interface name. For example to * "{0}.rpc" if you want to map GWT-RPC calls to "*.rpc" in web.xml, for example when using Spring dispatch servlet * to handle RPC requests. */ @Parameter(defaultValue = "{0}", property = "gwt.rpcPattern") private String rpcPattern; /** * Stop the build on error */ @Parameter(defaultValue = "true", property = "maven.gwt.failOnError") private boolean failOnError; /** * Pattern for GWT service interface */ @Parameter(defaultValue = "false", property = "generateAsync.force") private boolean force; @Parameter(property = "project.build.sourceEncoding") private String encoding; @Component private BuildContext buildContext; @Override protected boolean isGenerator() { return true; } public void execute() throws MojoExecutionException { if ( "pom".equals( getProject().getPackaging() ) ) { getLog().info( "GWT generateAsync is skipped" ); return; } setupGenerateDirectory(); if ( encoding == null ) { getLog().warn( "Encoding is not set, your build will be platform dependent" ); encoding = Charset.defaultCharset().name(); } JavaDocBuilder builder = createJavaDocBuilder(); List<String> sourceRoots = getProject().getCompileSourceRoots(); for ( String sourceRoot : sourceRoots ) { try { scanAndGenerateAsync( new File( sourceRoot ), builder ); } catch ( Throwable e ) { getLog().error( "Failed to generate Async interface", e ); if ( failOnError ) { throw new MojoExecutionException( "Failed to generate Async interface", e ); } } } } /** * @param sourceRoot the base directory to scan for RPC services * @return true if some file have been generated * @throws Exception generation failure */ private boolean scanAndGenerateAsync( File sourceRoot, JavaDocBuilder builder ) throws Exception { Scanner scanner = buildContext.newScanner( sourceRoot ); scanner.setIncludes( new String[] { servicePattern } ); scanner.scan(); String[] sources = scanner.getIncludedFiles(); if ( sources.length == 0 ) { return false; } boolean fileGenerated = false; for ( String source : sources ) { File sourceFile = new File( sourceRoot, source ); File targetFile = getTargetFile( source ); if ( !force && buildContext.isUptodate( targetFile, sourceFile ) ) { getLog().debug( targetFile.getAbsolutePath() + " is up to date. Generation skipped" ); // up to date, but still need to report generated-sources directory as sourceRoot fileGenerated = true; continue; } String className = getTopLevelClassName( source ); JavaClass clazz = builder.getClassByName( className ); if ( isEligibleForGeneration( clazz ) ) { getLog().debug( "Generating async interface for service " + className ); targetFile.getParentFile().mkdirs(); generateAsync( clazz, targetFile ); fileGenerated = true; } } return fileGenerated; } private File getTargetFile( String source ) { String targetFileName = source.substring( 0, source.length() - 5 ) + "Async.java"; File targetFile = new File( getGenerateDirectory(), targetFileName ); return targetFile; } /** * @param clazz the RPC service java class * @param targetFile RemoteAsync file to generate * @throws Exception generation failure */ private void generateAsync( JavaClass clazz, File targetFile ) throws IOException { PrintWriter writer = new PrintWriter( new BufferedWriter( new OutputStreamWriter( buildContext.newFileOutputStream( targetFile ), encoding ) ) ); boolean hasRemoteServiceRelativePath = hasRemoteServiceRelativePath(clazz); String className = clazz.getName(); if ( clazz.getPackage() != null ) { writer.println( "package " + clazz.getPackageName() + ";" ); writer.println(); } writer.println( "import com.google.gwt.core.client.GWT;" ); writer.println( "import com.google.gwt.user.client.rpc.AsyncCallback;" ); if (!hasRemoteServiceRelativePath) { writer.println( "import com.google.gwt.user.client.rpc.ServiceDefTarget;" ); } writer.println(); writer.println( "public interface " + className + "Async" ); writer.println( "{" ); JavaMethod[] methods = clazz.getMethods( true ); for ( JavaMethod method : methods ) { boolean deprecated = isDeprecated( method ); writer.println( "" ); writer.println( " /**" ); writer.println( " * GWT-RPC service asynchronous (client-side) interface" ); writer.println( " * @see " + clazz.getFullyQualifiedName() ); if ( deprecated ) writer.println( " * @deprecated" ); writer.println( " */" ); if ( deprecated ) writer.println( " @Deprecated" ); if ( returnRequest ) { writer.print( " com.google.gwt.http.client.Request " + method.getName() + "( " ); } else { writer.print( " void " + method.getName() + "( " ); } JavaParameter[] params = method.getParameters(); for ( int j = 0; j < params.length; j++ ) { JavaParameter param = params[j]; if ( j > 0 ) { writer.print( ", " ); } writer.print( method.getParameterTypes( true )[j].getGenericValue() ); if ( param.getType().getDimensions() != method.getParameterTypes( true )[j].getDimensions() ) { for ( int dimensions = 0; dimensions < param.getType().getDimensions(); dimensions++ ) { writer.print( "[]" ); } } writer.print( " " + param.getName() ); } if ( params.length > 0 ) { writer.print( ", " ); } if ( method.getReturnType().isVoid() ) { writer.println( "AsyncCallback<Void> callback );" ); } else if ( method.getReturnType().isPrimitive() ) { String primitive = method.getReturnType().getGenericValue(); writer.println( "AsyncCallback<" + WRAPPERS.get( primitive ) + "> callback );" ); } else { Type returnType = method.getReturnType( true ); String type = returnType.getGenericValue(); if ( method.getReturnType().getDimensions() != method.getReturnType( true ).getDimensions() ) { for ( int dimensions = 0; dimensions < method.getReturnType().getDimensions(); dimensions++ ) { type += "[]"; } } writer.println( "AsyncCallback<" + type + "> callback );" ); } writer.println(); } writer.println(); writer.println( " /**" ); writer.println( " * Utility class to get the RPC Async interface from client-side code" ); writer.println( " */" ); writer.println( " public static final class Util " ); writer.println( " { " ); writer.println( " private static " + className + "Async instance;" ); writer.println(); writer.println( " public static final " + className + "Async getInstance()" ); writer.println( " {" ); writer.println( " if ( instance == null )" ); writer.println( " {" ); writer.println( " instance = (" + className + "Async) GWT.create( " + className + ".class );" ); if ( !hasRemoteServiceRelativePath ) { String uri = MessageFormat.format( rpcPattern, className ); writer.println( " ServiceDefTarget target = (ServiceDefTarget) instance;" ); writer.println( " target.setServiceEntryPoint( GWT.getModuleBaseURL() + \"" + uri + "\" );" ); } writer.println( " }" ); writer.println( " return instance;" ); writer.println( " }" ); writer.println( "" ); writer.println( " private Util()" ); writer.println( " {" ); writer.println( " // Utility class should not be instantiated" ); writer.println( " }" ); writer.println( " }" ); writer.println( "}" ); writer.close(); } private boolean isEligibleForGeneration( JavaClass javaClass ) { return javaClass.isInterface() && javaClass.isPublic() && javaClass.isA( REMOTE_SERVICE_INTERFACE ); } private JavaDocBuilder createJavaDocBuilder() throws MojoExecutionException { JavaDocBuilder builder = new JavaDocBuilder(); builder.setEncoding( encoding ); builder.getClassLibrary().addClassLoader( getProjectClassLoader() ); for ( String sourceRoot : getProject().getCompileSourceRoots() ) { builder.getClassLibrary().addSourceFolder( new File( sourceRoot ) ); } return builder; } private String getTopLevelClassName( String sourceFile ) { String className = sourceFile.substring( 0, sourceFile.length() - 5 ); // strip ".java" return className.replace( File.separatorChar, '.' ); } /** * Determine if a client service method is deprecated. * * @see MGWT-352 */ private boolean isDeprecated( JavaMethod method ) { if ( method == null ) return false; for ( Annotation annotation : method.getAnnotations() ) { if ( "java.lang.Deprecated".equals( annotation.getType().getFullyQualifiedName() ) ) { return true; } } return method.getTagByName( "deprecated" ) != null; } private boolean hasRemoteServiceRelativePath(final JavaClass clazz) { if ( clazz != null && clazz.getAnnotations() != null ) { for ( Annotation annotation : clazz.getAnnotations() ) { getLog().debug( "annotation found on service interface " + annotation ); if ( annotation.getType().getValue().equals( "com.google.gwt.user.client.rpc.RemoteServiceRelativePath" ) ) { getLog().debug( "@RemoteServiceRelativePath annotation found on service interface" ); return true; } } } return false; } private ClassLoader getProjectClassLoader() throws MojoExecutionException { Collection<File> classpath = getClasspath( Artifact.SCOPE_COMPILE ); URL[] urls = new URL[classpath.size()]; try { int i = 0; for ( File classpathFile : classpath ) { urls[i] = classpathFile.toURI().toURL(); i++; } } catch ( MalformedURLException e ) { throw new MojoExecutionException( e.getMessage(), e ); } return new URLClassLoader( urls, ClassLoader.getSystemClassLoader() ); } }